<?php

declare(strict_types=1);

namespace app\service\product;

use app\model\ProductImportJob;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Worksheet\Row;
use think\facade\Log;

class ProductImportJobService
{
    private const STATUS_LABELS = [
        ProductImportJob::STATUS_PENDING => '排队中',
        ProductImportJob::STATUS_RUNNING => '执行中',
        ProductImportJob::STATUS_SUCCESS => '已完成',
        ProductImportJob::STATUS_FAILED => '失败',
    ];
    public function __construct(
        private ProdService $prodService,
        private ProductImportStorageService $storageService
    ) {
    }

    public function createJob(
        array $basePayload,
        string $filePath,
        string $fileHash,
        array $mapping,
        array $options = [],
        ?int $chunkSize = null,
        int $totalRows = 0,
        bool $allowDuplicate = false
    ): ProductImportJob {
        if (!isset($basePayload['seller_org_id'])) {
            throw new \InvalidArgumentException('缺少卖家组织参数');
        }
        $normalizedMapping = [];
        foreach ($mapping as $field => $header) {
            if (!is_string($header)) {
                continue;
            }
            $normalized = trim($header);
            if ($normalized !== '') {
                $normalizedMapping[$field] = $normalized;
            }
        }

        if ($normalizedMapping === []) {
            throw new \InvalidArgumentException('字段映射不能为空');
        }
        $mapping = $normalizedMapping;

        $chunkSize ??= (int) config('product.bulk_chunk_size', 20);
        $chunkSize = $chunkSize > 0 ? $chunkSize : 20;

        $payloadHash = hash(
            'sha256',
            $fileHash . json_encode($mapping, JSON_UNESCAPED_UNICODE) . json_encode($basePayload, JSON_UNESCAPED_UNICODE)
        );

        $existing = ProductImportJob::where('seller_org_id', (int) $basePayload['seller_org_id'])
            ->where('payload_hash', $payloadHash)
            ->whereIn('status', [ProductImportJob::STATUS_PENDING, ProductImportJob::STATUS_RUNNING])
            ->find();

        if ($existing) {
            $existing->setAttr('duplicate', true);
            return $existing;
        }

        $latestJob = ProductImportJob::where('seller_org_id', (int) $basePayload['seller_org_id'])
            ->where('payload_hash', $payloadHash)
            ->order('id', 'desc')
            ->find();

        if ($latestJob && !$allowDuplicate) {
            $statusLabel = $this->translateStatus((string) $latestJob->status);
            $message = sprintf(
                '[DUPLICATE_IMPORT_CONFIRM] 已存在相同参数的导入任务（ID %d，状态 %s，创建于 %s），确认重复导入可能会产生重复商品，请确认后重试。',
                $latestJob->id,
                $statusLabel,
                $latestJob->create_time
            );
            throw new \InvalidArgumentException($message);
        }

        $jobData = [
            'seller_org_id' => (int) $basePayload['seller_org_id'],
            'seller_user_id' => $basePayload['seller_user_id'] ?? null,
            'status' => ProductImportJob::STATUS_PENDING,
            'chunk_size' => $chunkSize,
            'total_rows' => max(0, $totalRows),
            'file_path' => $filePath,
            'file_hash' => $fileHash,
            'payload_hash' => $payloadHash,
            'mapping' => $mapping,
            'options' => array_merge($options, [
                'base_payload' => $basePayload,
            ]),
            'notify_status' => ProductImportJob::NOTIFY_PENDING,
        ];
        try {
            $job = ProductImportJob::create($jobData);
        } catch (\Throwable $e) {
            Log::error(sprintf(
                '[ProductImportJob] create failed: %s in %s:%d trace=%s',
                $e->getMessage(),
                $e->getFile(),
                $e->getLine(),
                $e->getTraceAsString()
            ));
            throw $e;
        }

        $job->setAttr('duplicate', false);

        return $job;
    }

    public function processPendingJobs(int $limit = 1): array
    {
        $jobs = ProductImportJob::where('status', ProductImportJob::STATUS_PENDING)
            ->order('id')
            ->limit($limit)
            ->select();

        $stats = [
            'total' => count($jobs),
            'processed' => 0,
            'failed' => 0,
        ];

        foreach ($jobs as $job) {
            if (!$this->markJobRunning($job)) {
                continue;
            }

            try {
                $this->runJob($job->refresh());
                $stats['processed']++;
            } catch (\Throwable $e) {
                $this->markJobFailed($job, $e->getMessage());
                Log::error(sprintf('Product import job [%d] failed: %s', $job->id, $e->getMessage()), [
                    'job_id' => $job->id,
                    'exception' => $e,
                ]);
                $stats['failed']++;
            }
        }

        return $stats;
    }

    private function markJobRunning(ProductImportJob $job): bool
    {
        $updated = ProductImportJob::where('id', $job->id)
            ->where('status', ProductImportJob::STATUS_PENDING)
            ->update([
                'status' => ProductImportJob::STATUS_RUNNING,
                'started_at' => date('Y-m-d H:i:s'),
            ]);

        return $updated > 0;
    }

    private function runJob(ProductImportJob $job): void
    {
        @set_time_limit(0);

        $mapping = (array) ($job->mapping ?? []);
        if ($mapping === []) {
            throw new \RuntimeException('任务缺少字段映射');
        }

        $basePayload = (array) (($job->options['base_payload'] ?? []) ?: []);
        $filePath = $this->storageService->getLocalPath((string) $job->file_path);
        if (!is_file($filePath)) {
            throw new \RuntimeException('导入文件不存在或已被删除');
        }

        $chunkSize = (int) ($job->chunk_size ?: config('product.bulk_chunk_size', 20));
        $chunkSize = $chunkSize > 0 ? $chunkSize : 20;

        $reader = IOFactory::createReaderForFile($filePath);
        $reader->setReadDataOnly(true);
        $spreadsheet = $reader->load($filePath);
        $sheet = $spreadsheet->getActiveSheet();

        $rowIterator = $sheet->getRowIterator();
        $headerLookup = [];
        $dataRowIndex = 0;
        $buffer = [];

        foreach ($rowIterator as $row) {
            /** @var Row $row */
            $rowNumber = $row->getRowIndex();
            $values = $this->extractRowValues($row);

            if ($rowNumber === 1) {
                $headerLookup = $this->buildHeaderLookup($values);
                $this->validateMappingHeaders($mapping, $headerLookup);
                continue;
            }

            $dataRowIndex++;

            if ($dataRowIndex <= (int) $job->current_index) {
                continue;
            }

            $rowData = $this->mapRowValues($mapping, $headerLookup, $values);
            $rowData['__sheet_row'] = $rowNumber;
            $buffer[] = $rowData;

            if (count($buffer) >= $chunkSize) {
                $this->processChunk($job, $basePayload, $buffer);
                $job->current_index += count($buffer);
                $job->save();
                $buffer = [];
            }
        }

        if ($buffer !== []) {
            $this->processChunk($job, $basePayload, $buffer);
            $job->current_index += count($buffer);
            $job->save();
        }

        if ($job->total_rows === 0 || $job->total_rows !== $dataRowIndex) {
            $job->total_rows = $dataRowIndex;
        }

        $job->status = ProductImportJob::STATUS_SUCCESS;
        $job->finished_at = date('Y-m-d H:i:s');
        $job->notify_status = ProductImportJob::NOTIFY_PENDING;
        $job->error_log = $job->error_log ?? [];
        $job->save();

        $spreadsheet->disconnectWorksheets();
        unset($spreadsheet);
    }

    private function processChunk(ProductImportJob $job, array $basePayload, array $chunk): void
    {
        $errors = $job->error_log ?? [];
        if (!is_array($errors)) {
            $errors = [];
        }

        foreach ($chunk as $row) {
            $sheetRow = $row['__sheet_row'] ?? null;
            unset($row['__sheet_row']);

            try {
                $payload = array_merge($basePayload, $row);
                $payload['seller_org_id'] = $payload['seller_org_id'] ?? (int) $job->seller_org_id;
                $payload['seller_user_id'] = $payload['seller_user_id'] ?? $job->seller_user_id;

                $this->prodService->create($payload);
                $job->success_count++;
            } catch (\Throwable $e) {
                $job->fail_count++;
                $errors[] = sprintf(
                    '第 %d 行：%s',
                    $sheetRow ?: ($job->current_index + 1),
                    $e->getMessage()
                );
                $errors = array_slice($errors, -200);
            }
        }

        $job->error_log = $errors;
        $job->save();
    }

    private function markJobFailed(ProductImportJob $job, string $message): void
    {
        $errors = $job->error_log ?? [];
        if (!is_array($errors)) {
            $errors = [];
        }
        $errors[] = $message;
        $errors = array_slice($errors, -200);

        $job->status = ProductImportJob::STATUS_FAILED;
        $job->finished_at = date('Y-m-d H:i:s');
        $job->error_log = $errors;
        $job->notify_status = ProductImportJob::NOTIFY_PENDING;
        $job->save();
    }

    private function extractRowValues(Row $row): array
    {
        $values = [];
        $cellIterator = $row->getCellIterator();
        $cellIterator->setIterateOnlyExistingCells(false);

        foreach ($cellIterator as $cell) {
            $values[] = $cell ? $cell->getCalculatedValue() : null;
        }

        return $values;
    }

    private function buildHeaderLookup(array $headers): array
    {
        $lookup = [];
        foreach ($headers as $index => $header) {
            $normalized = $this->normalizeHeader((string) $header);
            if ($normalized === '') {
                continue;
            }
            $lookup[$normalized] = $index;
        }

        return $lookup;
    }

    private function validateMappingHeaders(array $mapping, array $headerLookup): void
    {
        foreach ($mapping as $header) {
            $normalized = $this->normalizeHeader((string) $header);
            if (!array_key_exists($normalized, $headerLookup)) {
                throw new \RuntimeException(sprintf('表头 "%s" 在文件中不存在', $header));
            }
        }
    }

    private function mapRowValues(array $mapping, array $headerLookup, array $rowValues): array
    {
        $result = [];
        foreach ($mapping as $field => $header) {
            $normalized = $this->normalizeHeader((string) $header);
            if (!array_key_exists($normalized, $headerLookup)) {
                throw new \RuntimeException(sprintf('表头 "%s" 在文件中不存在', $header));
            }
            $columnIndex = $headerLookup[$normalized];
            $value = $rowValues[$columnIndex] ?? null;
            if (is_string($value)) {
                $value = trim($value);
            }
            $result[$field] = $value;
        }

        return $result;
    }

    private function normalizeHeader(string $header): string
    {
        return preg_replace('/\s+/', '', mb_strtolower($header)) ?? '';
    }

    private function translateStatus(string $status): string
    {
        return self::STATUS_LABELS[$status] ?? $status;
    }
}
